/*****************************************************************************
 *   Copyright (C) 2008 John Schember <john@nachtimwald.com>                 *
 *                                                                           *
 *   This file is part of niwbillmanager.                                    *
 *                                                                           *
 *   niwbillmanager is free software: you can redistribute it and/or         *
 *   modify it under the terms of the GNU General Public License as          *
 *   published by the Free Software Foundation, either version 3 of the      *
 *   License, or (at your option) any later version.                         *
 *                                                                           *
 *   niwbillmanager is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of          *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the            *
 *   GNU General Public License for more details.                            *
 *                                                                           *
 *   You should have received a copy of the GNU General Public License       *
 *   along with niwbillmanager. If not, see                                  *
 *   <http://www.gnu.org/licenses/>.                                         *
 *****************************************************************************/

#include <QFile>
#include <QFileDialog>
#include <QIcon>
#include <QMultiHash>
#include <QPrintDialog>
#include <QPrinter>
#include <QPrintPreviewDialog>
#include <QSet>
#include <QTextStream>
#include <QWebFrame>
#include <QWebPage>

#include "reportwindow.h"

int ReportWindow::instanceCount = 0;

ReportWindow::ReportWindow()
{
    setupUi(this);

    setAttribute(Qt::WA_DeleteOnClose, true);

    m_lib = 0;

    ReportWindow::instanceCount++;
    if (ReportWindow::instanceCount > 1) {
        setWindowTitle(tr("Bill Report") + " <"
            + QString::number(ReportWindow::instanceCount) + ">");
    }

    findWidget->setCloseIcon(QIcon(":/plugin-reports/icons/window-close.png"));
    findWidget->setPreviousIcon(QIcon(
        ":/plugin-reports/icons/go-previous.png"));
    findWidget->setNextIcon(QIcon(":/plugin-reports/icons/go-next.png"));

    findFrame->hide();

    connectSignals();
}

ReportWindow::~ReportWindow()
{
    ReportWindow::instanceCount--;
}

void ReportWindow::generateReport()
{
    // We can't get any bill items if the lib is unavaliable.
    if (!m_lib) {
        return;
    }

    // Used to create the html document. It's created from separate parts so
    // that various parts can be created at the same time then assembled in the
    // correct order later.
    QString top;
    QString monthHeading;
    QString settingsHeading;
    QString navLinksMonthListing;
    QString navLinksYearListing;
    QString navLinksCharts;
    QString monthListing;
    QString yearListing;
    QString charts;
    QString bottom;

    // int - the day the bill is due on.
    QMultiHash<int, BillItem> recurringMonthItems;
    QMultiHash<int, BillItem> recurringLastDayMonthItems;
    // int - the month the bill is from.
    QMultiHash<int, BillItem> recurringYearlyItems;

    // Store the items for each year.
    QHash<int, QString> yearItems;
    QHash<int, double> yearTotals;
    // Store data for the charts.
    QHash<QString, double> chartTagsProportion;
    QHash<QString, double> chartTagsAmount;
    QHash<QString, double> chartPayee;
    QHash<QString, double> chartPaid;
    QHash<QString, double> chartAutoPay;
    QHash<QString, double> chartRecurring;
    QHash<QString, double> chartTotals;

    // Get the top part of the html. This includes the Javascript charting
    // functionality.
    QFile topFile(":/plugin-reports/html/top.html");
    topFile.open(QIODevice::ReadOnly);
    top = topFile.readAll();
    topFile.close();

    // Get the bottom part of the html.
    QFile bottomFile(":/plugin-reports/html/bottom.html");
    bottomFile.open(QIODevice::ReadOnly);
    bottom = bottomFile.readAll();
    bottomFile.close();

    // Create the sub-heading that lists all of the selected months.
    monthHeading = "<h3>";
    Q_FOREACH (QDate date, m_dates) {
        // Set the sub-heading consisting of a list of months (with year) that
        // this report is using.
        monthHeading += QDate::longMonthName(date.month()) + " "
            + date.toString("yyyy") + " , ";
    }
    if (monthHeading.endsWith(" , ")) {
        monthHeading.chop(3);
    }
    monthHeading += "</h3>";

    // Create the sub-heading that lists the selected settings.
    settingsHeading = "<h4>";
    if (m_options.optionPaid) {
        settingsHeading += tr(" Paid,");
    }
    if (m_options.optionUnpaid) {
        settingsHeading += tr(" Unpaid,");
    }
    if (m_options.optionRecurring) {
        settingsHeading += tr(" Recurring,");
    }
    if (m_options.optionNonRecurring) {
        settingsHeading += tr(" Non-Recurring,");
    }
    if (m_options.optionAutoPay) {
        settingsHeading += tr(" Auto Pay,");
    }
    if (m_options.optionNonAutoPay) {
        settingsHeading += tr(" Non-Auto Pay,");
    }
    if (settingsHeading.endsWith(tr(","))) {
        settingsHeading.chop(1);
    }
    settingsHeading += "</h4>";

    // Start the html for listing by month if necessary.
    if (m_options.listingMonth) {
        monthListing += "<h3 id=\"listbymonth\">Listing by Month</h3>";
        navLinksMonthListing += "<a href=\"#listbymonth\">"
            + tr("Listing by Month") + "</a><br /><ul>";
    }

    // Get all the recurring bills.
    if (m_options.optionRecurring) {
        QSet<QString> emptyQuery;
        m_lib->startSearchBillItemsAnd(emptyQuery, false,
            m_options.optionAutoPay, m_options.optionNonAutoPay, false, false,
            false, false, false, false, false, false,
            true, false,
            m_options.optionPaid, m_options.optionUnpaid, false, false, false);

        while (m_lib->hasMoreBillItems()) {
            BillItem billItem;

            m_lib->getNextBillItem(billItem);
            // Store the month objects.
            switch (billItem.getPaymentRecurringPeriod()) {
                case RecurringPeriodType::MONTHLY:
                    recurringMonthItems.insert(billItem.getDateDue().day(),
                        billItem);
                    break;
                case RecurringPeriodType::LASTDAYOFMONTH:
                    recurringLastDayMonthItems.insert(
                        billItem.getDateDue().day(), billItem);
                    break;
                case RecurringPeriodType::YEARLY:
                    recurringYearlyItems.insert(
                        billItem.getDateDue().month(), billItem);
                    break;
                // Bills that due not recurr.
                default:
                    break;
            }
        }

    }

    // Run though every date (month - year) that the user selected.
    // m_dates must be sorted with the earliest date first and the latest
    // date last. This is because a running list of recurring items will be
    // kept and added to subquent months and years.
    Q_FOREACH (QDate date, m_dates) {
        // Since we are looping though a single month we will get the total
        // for all items.
        double monthTotal = 0;

        // Set the query (yyyy-MM) and run it. yyyy-MM is iso date format.
        // startSearchBillItemsAnd takes a QSet as the query so we need to
        // put the query into a QSet.
        QSet<QString> dateQuery;
        dateQuery.insert(QString("%1-%2").arg(date.toString("yyyy")).arg(date
            .toString("MM")));
        m_lib->startSearchBillItemsAnd(dateQuery, false,
            m_options.optionAutoPay, m_options.optionNonAutoPay, true, false,
            false, false, false, false, false, false,
            m_options.optionRecurring, m_options.optionNonRecurring,
            m_options.optionPaid, m_options.optionUnpaid, false, false, false);

        if (m_options.listingMonth) {
            // Set the heading and start the table for month listing.
            monthListing += QString("<h4 id=\"listing-%1%2\">%1 %2</h4>"
                "<table width=\"100%\">").arg(QDate::longMonthName(date
                .month())).arg(QString::number(date.year()));
            navLinksMonthListing += QString("<li><a href=\"#listing-%1%2\">"
                "%1 %2</a></li>").arg(QDate::longMonthName(date.month()))
                .arg(QString::number(date.year()));
            monthListing += "<tr><td><u>" + tr("Name") + "</u>:</td><td><u>"
                + tr("Day Due") + "</u>:</td><td><u>" + tr("Recurring")
                + "</u>:</td><td><u>" + tr("Payee") + "</u>:</td><td><u>"
                + tr("Method") + "</u>:</td><td align=\"right\"><u>"
                + tr("Amount Due") + "</u>:</td>";
        }

        // int - day of the month that the item is due.
        // Store the non-recurring items for this month. Assemble it with the
        // recurring items later.
        QMultiHash<int, BillItem> monthItems;

        // Get all the bill items for the month.
        while (m_lib->hasMoreBillItems()) {
            BillItem billItem;

            m_lib->getNextBillItem(billItem);

            // Be sure only bill items for the correct month are used.
            if (m_options.listingMonth || m_options.listingYear
                && billItem.getDateDue().month() == date.month()
                && billItem.getDateDue().year() == date.year())
            {
                // Store the month objects.
                switch (billItem.getPaymentRecurringPeriod()) {
                    // Recurring bills were stored eariler.
                    case RecurringPeriodType::NONE:
                        monthItems.insert(billItem.getDateDue().day(),
                            billItem);
                        break;
                    default:
                        break;
                }
            }
        }

        // Combine all the month items into a single multi hash. This is so
        // The items can be sorted.
        QMultiHash<int, BillItem> allMonthItems;
        Q_FOREACH (int key, recurringMonthItems.uniqueKeys()) {
            Q_FOREACH (BillItem billItem, recurringMonthItems.values(key)) {
                if (billItem.getDateDue().year() <= date.year()) {
                    // Find the last day of the month.
                    QDate lastDay(date.year(), date.month(), 1);
                    lastDay = lastDay.addMonths(1).addDays(-1);
                    // Check to see if the bill item's date is past the last
                    // day of the month.
                    if (billItem.getDateDue().day() > lastDay.day()) {
                        billItem.setDateDue(lastDay);
                    }
                    allMonthItems.insert(billItem.getDateDue().day(),
                        billItem);
                }
            }
        }
        Q_FOREACH (int key, recurringLastDayMonthItems.uniqueKeys()) {
            Q_FOREACH (BillItem billItem,
                recurringLastDayMonthItems.values(key))
            {
                if (billItem.getDateDue().year() <= date.year()) {
                // Find the last day of the month.
                QDate lastDay(date.year(), date.month(), 1);
                lastDay = lastDay.addMonths(1).addDays(-1);
                // Set the last day of the month for the item.
                billItem.setDateDue(lastDay);
                allMonthItems.insert(lastDay.day(), billItem);
                }
            }
        }
        Q_FOREACH (int key, recurringYearlyItems.uniqueKeys()) {
            // We only want items that are for this month.
            if (key == date.month()) {
                Q_FOREACH (BillItem billItem, recurringYearlyItems.values(
                    key))
                {
                    if (billItem.getDateDue().year() <= date.year()) {
                        allMonthItems.insert(billItem.getDateDue().day(),
                            billItem);
                    }
                }
            }
        }
        Q_FOREACH (int key, monthItems.uniqueKeys()) {
            Q_FOREACH (BillItem billItem, monthItems.values(key))
            {
                allMonthItems.insert(key, billItem);
            }
        }

        // Get all the days for this month and sort the list.
        QList<int> days = allMonthItems.uniqueKeys();
        qSort(days);

        // Create the html for the month, year and get the totals.
        Q_FOREACH (int day, days) {
            Q_FOREACH (BillItem billItem, allMonthItems.values(day)) {
                if (m_options.listingMonth) {
                    monthListing += QString("<tr><td>%1</td><td>%2</td>"
                        "<td>%3</td><td>%4</td><td>%5</td><td align="
                        "\"right\">%6</td></tr>").arg(billItem.getName())
                        .arg(billItem.getDateDue().toString("dd")).arg(
                        RecurringPeriodType::recurringPeriodTypeToString(
                        billItem.getPaymentRecurringPeriod())).arg(billItem
                        .getPayee()).arg(billItem.getPaymentMethod()).arg(
                        QString::number(billItem.getAmountDue()));
                }
                if (m_options.listingMonth || m_options.chartTotals) {
                    monthTotal += billItem.getAmountDue();
                }

                if (m_options.listingYear)
                {
                    // Add the item to the list for it's year.
                    yearItems.insert(billItem.getDateDue().year(),
                        yearItems.value(billItem.getDateDue().year())
                        + QString("<tr><td>%1</td><td>%2</td><td>%3</td>"
                        "<td>%4</td><td>%5</td><td align=\"right\">%6</td>"
                        "</tr>").arg(billItem.getName()).arg(billItem
                        .getDateDue().toString("MMMM dd")).arg(
                        RecurringPeriodType::recurringPeriodTypeToString(
                        billItem.getPaymentRecurringPeriod())).arg(billItem
                        .getPayee()).arg(billItem.getPaymentMethod()).arg(
                        QString::number(billItem.getAmountDue())));
                    // Add the amount to the total for the year.
                    yearTotals.insert(billItem.getDateDue().year(),
                        yearTotals.value(billItem.getDateDue().year())
                        + billItem.getAmountDue());
                }

                // Store the chart information.
                if (m_options.chartTags) {
                    // Get the tags for the bill item.
                    Q_FOREACH (QString tag, billItem.getTags()) {
                        chartTagsProportion.insert(tag, chartTagsProportion.value(
                            tag) + 1);
                        chartTagsAmount.insert(tag, chartTagsAmount.value(tag)
                            + billItem.getAmountDue());
                    }
                }
                if (m_options.chartPayee) {
                    chartPayee.insert(billItem.getPayee(), chartPayee.value(
                        billItem.getPayee()) + 1);
                }
                if (m_options.chartPaid) {
                    chartPaid.insert((billItem.getPaid()?"Paid":"Unpaid"),
                        chartPaid.value(billItem.getPaid()?"Paid":"Unpaid") + 1);
                }
                if (m_options.chartAutoPay) {
                    chartAutoPay.insert(
                        (billItem.getAutoPay()?"Auto Pay":"Non-Auto Pay"),
                        chartAutoPay.value(
                        billItem.getAutoPay()?"Auto Pay":"Non-Auto Pay") + 1);
                }
                if (m_options.chartRecurring) {
                    chartRecurring.insert(
                        RecurringPeriodType::recurringPeriodTypeToString(
                        billItem.getPaymentRecurringPeriod()), chartRecurring
                        .value(RecurringPeriodType::recurringPeriodTypeToString(
                        billItem.getPaymentRecurringPeriod())) + 1);
                }
            }
        }

        // Write the total and close the table for the month.
        if (m_options.listingMonth) {
            monthListing += QString("<tr class=\"listingtotal\"><td><b>"
                + tr("Total") + "</b>:</td><td colspan=\"5\" align=\"right\">"
                "%1</td></tr>").arg(QString::number(monthTotal));
            monthListing += "</table>";
        }

        // Set the total for the totals chart.
        if (m_options.chartTotals) {
            chartTotals.insert(date.toString("MMMM-yyyy"), monthTotal);
        }
    }

    // close the listing for the links to the months in the list.
    if (m_options.listingMonth) {
        navLinksMonthListing += "</ul>";
    }

    if (m_options.listingYear) {
        yearListing += "<h3 id=\"listbyyear\">" + tr("Listing by Year</h3>");
        navLinksYearListing += "<a href=\"#listbyyear\">" + tr("Listing by Year")
            + "</a><br /><ul>";

        // Run though the data and add the items to the list for the associated
        // year.
        Q_FOREACH (int key, yearItems.uniqueKeys()) {
            yearListing += QString("<h4 id=\"listing-%1\">%1</h4><table "
                "width=\"100%\">").arg(QString::number(key));
            yearListing += "<tr><td><u>" + tr("Name") + "</u>:</td><td><u>"
                + tr("Day Due") + "</u>:</td></td><td><u>" + tr("Recurring")
                + "</u>:</td><td><u>" + tr("Payee") + "</u>:</td><td><u>"
                + tr("Method") + "</u>:</td><td align=\"right\"><u>"
                + tr("Amount Due") + "</u>:</td>";
            navLinksYearListing += QString("<li><a href=\"#listing-%1\">%1</a>"
                "</li>").arg(QString::number(key));
            yearListing += yearItems.value(key);
            yearListing += QString("<tr class=\"listingtotal\"><td><b>"
                + tr("Total") + "</b>:</td><td colspan=\"5\" align=\"right\">"
                "%1</td></tr>").arg(QString::number(yearTotals.value(key)));
            yearListing += "</table>";
        }

        navLinksYearListing += "</ul>";
    }

    if (m_options.chartTags || m_options.chartPayee || m_options.chartPaid
        || m_options.chartAutoPay || m_options.chartRecurring
        || m_options.chartTotals)
    {
        // Setup links, header, divs, and scripts.
        navLinksCharts += "<a href=\"#charts\">" + tr("Charts")
             + "</a><br /><ul>";
        charts += "<h3 id=\"charts\">" + tr("Charts") + "</h3>";
        if (m_options.chartTags) {
            navLinksCharts += "<li><a href=\"#charttags1\">"
                + tr("Tags by Occurrence") + "</a></li>";
            navLinksCharts += "<li><a href=\"#charttags2\">"
                + tr("Tags by Monetary Amount") + "</a></li>";
            charts += "<h3>" + tr("Tags by Occurrence") + "</h3>";
            charts += "<div id=\"charttags1\"></div>";
            charts += "<h3>" + tr("Tags by Monetary Amount") + "</h3>";
            charts += "<div id=\"charttags2\"></div>";
            charts += generateChartJavascriptFunction("pie",
                "chartTagsProportion", chartTagsProportion, "charttags1");
            charts += generateChartJavascriptFunction("pie", "chartTagsAmount",
                chartTagsAmount, "charttags2");
        }
        if (m_options.chartPayee) {
            navLinksCharts += "<li><a href=\"#chartpayee\">" + tr("Payee")
                + "</a></li>";
            charts += "<h3>"+ tr("Payee") + "</h3>";
            charts += "<div id=\"chartpayee\"></div>";
            charts += generateChartJavascriptFunction("pie", "chartPayee",
                chartPayee, "chartpayee");
        }
        if (m_options.chartPaid) {
            navLinksCharts += "<li><a href=\"#chartpaid\">" + tr("Paid")
                + "</a></li>";
            charts += "<h3>"+ tr("Paid") + "</h3>";
            charts += "<div id=\"chartpaid\"></div>";
            charts += generateChartJavascriptFunction("pie", "chartPaid",
                chartPaid, "chartpaid");
        }
        if (m_options.chartAutoPay) {
            navLinksCharts += "<li><a href=\"#chartautopay\">" + tr("Auto Pay")
                + "</a></li>";
            charts += "<h3>" + tr("Auto Pay") + "</h3>";
            charts += "<div id=\"chartautopay\"></div>";
            charts += generateChartJavascriptFunction("pie", "chartAutoPay",
                chartAutoPay, "chartautopay");
        }
        if (m_options.chartRecurring) {
            navLinksCharts += "<li><a href=\"#chartrecurring\">"
                + tr("Recurring") + "</a></li>";
            charts += "<h3>" + tr("Recurring") + "</h3>";
            charts += "<div id=\"chartrecurring\"></div>";
            charts += generateChartJavascriptFunction("pie", "chartRecurring",
                chartRecurring, "chartrecurring");
        }
        if (m_options.chartTotals) {
            navLinksCharts += "<li><a href=\"#charttotals\">" + tr("Totals")
                + "</a></li>";
            charts += "<h3>" + tr("Totals") + "</h3>";
            charts += "<div id=\"charttotals\"></div>";
            charts += generateChartJavascriptFunction("bar", "chartTotals",
                chartTotals, "charttotals");
        }
        navLinksCharts += "</ul>";
    }

    webView->setHtml(top + monthHeading + settingsHeading
        + navLinksMonthListing + navLinksYearListing + navLinksCharts
        + monthListing + yearListing + charts + bottom);
}

void ReportWindow::setDates(QList<QDate> dates)
{
    m_dates = dates;
    qSort(m_dates);
}

void ReportWindow::setOptions(const ReportOptions &options)
{
    m_options = options;
}

void ReportWindow::setLib(NiwBillManager *lib)
{
    m_lib = lib;
}

void ReportWindow::on_actionSave_As_triggered()
{
    QString fileName;

    fileName = QFileDialog::getSaveFileName(this, tr("Save Report"),
        tr("report") + ".html", tr("Bill Reports") +  "(*.html)");

    if (!fileName.isEmpty()) {
        QFile file(fileName);

        if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream out(&file);
            out << webView->page()->mainFrame()->toHtml();
        }
        file.close();
    }
}

void ReportWindow::on_actionPrint_Preview_triggered()
{
    QPrinter printer(QPrinter::HighResolution);
    QPrintPreviewDialog previewDialog( &printer, this);

    connect(&previewDialog, SIGNAL(paintRequested(QPrinter *)),
        webView, SLOT(print(QPrinter *)));

    previewDialog.exec();

    disconnect(&previewDialog, SIGNAL(paintRequested(QPrinter *)),
        webView, SLOT(print(QPrinter *)));
}

void ReportWindow::on_actionPrint_triggered()
{
    QPrinter printer(QPrinter::HighResolution);
    QPrintDialog printDialog(&printer, this);
    printDialog.setWindowTitle(tr("Print Report"));

    printDialog.exec();
    if (printDialog.result() == QDialog::Accepted) {
        webView->print(&printer);
    }
}

void ReportWindow::on_actionFind_triggered()
{
    findFrame->show();
    findWidget->setFocus();
}

void ReportWindow::on_actionZoom_In_triggered()
{
    if (webView->textSizeMultiplier() < 12) {
        webView->setTextSizeMultiplier(webView->textSizeMultiplier() + 0.1);
    }
}

void ReportWindow::on_actionZoom_Out_triggered()
{
    if (webView->textSizeMultiplier() > 0.2) {
        webView->setTextSizeMultiplier(webView->textSizeMultiplier() - 0.1);
    }
}

void ReportWindow::on_actionReset_Zoom_triggered()
{
    webView->setTextSizeMultiplier(1.0);
}

void ReportWindow::findText(QString query, NiwFindWidget::NiwFindFlags flags)
{
    QWebPage::FindFlags options = QWebPage::FindWrapsAroundDocument;

    if ((flags & NiwFindWidget::BACKWARD) == NiwFindWidget::BACKWARD) {
        options |= QWebPage::FindBackward;
    }
    if ((flags & NiwFindWidget::CASESENSITIVE) == NiwFindWidget::CASESENSITIVE)
    {
        options |= QWebPage::FindCaseSensitively;
    }

    webView->findText(query, options);
}

void ReportWindow::hideFind()
{
    findFrame->hide();
}

void ReportWindow::connectSignals()
{
    connect(findWidget, SIGNAL(findText(QString, NiwFindWidget::NiwFindFlags)),
        this, SLOT(findText(QString, NiwFindWidget::NiwFindFlags)));
    connect(findWidget, SIGNAL(wantsToClose()), this, SLOT(hideFind()));
}

QString ReportWindow::generateChartJavascriptFunction(QString type,
    QString name, QHash<QString, double> data, QString divId)
{
    QString chart;
    QString xTicks;
    QString jsData;

    chart += "<script type=\"text/javascript\">";
    chart += QString("function %1() {").arg(name);

    xTicks += "var xTicks = [";
    jsData += "var data = [";
    QList<QString> dataList = data.uniqueKeys();
    // Sort the list. Reverse the order for the Totals chart.
    qSort(dataList.begin(), dataList.end(), qGreater<QString>());
    for (int i = 0; i < dataList.size(); i++) {
        xTicks += QString("{label: \"%1\", v: %2},").arg(dataList.at(i))
            .arg(i);
        jsData += QString("[%1, %2],").arg(i).arg(data.value(dataList.at(i)));
    }
    if (xTicks.endsWith(",")) {
        xTicks.chop(1);
    }
    if (jsData.endsWith(",")) {
        jsData.chop(1);
    }
    xTicks += "];";
    jsData += "];";

    chart += xTicks;
    chart += jsData;
    chart += QString("var layout = new PlotKit.Layout(\"%1\", {\"xTicks\": "
        "xTicks});").arg(type);
    chart += QString("layout.addDataset(\"%1\", data);").arg(divId);
    chart += "layout.evaluate();";
    chart += "var svg = SVGRenderer.SVG({\"width\": 500, \"height\": 400, "
        "\"id\":\"chart\"});";
    chart += QString("$('%1').appendChild(svg);").arg(divId);
    chart += "var chart = new SweetSVGRenderer(svg, layout);";
    chart += "chart.render();";
    chart += "}";
    chart += QString("MochiKit.DOM.addLoadEvent(%1);").arg(name);
    chart += "</script>";

    return chart;
}
